/**
 * Copyright (C) 2007 The AsyncMail Group <asyncmail@googlegroups.com>
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.asyncmail.smtpserver.handler;

import com.googlecode.asyncmail.common.DSNStatus;
import com.googlecode.asyncmail.common.MailAddress;
import com.googlecode.asyncmail.common.MailEnvelope;
import com.googlecode.asyncmail.smtp.SMTPResponse;
import com.googlecode.asyncmail.smtp.SMTPResponseImpl;
import com.googlecode.asyncmail.smtp.SMTPRetCode;
import com.googlecode.asyncmail.smtpserver.SMTPHandlerConfigurationData;
import com.googlecode.asyncmail.smtpserver.SMTPSession;
import com.googlecode.asyncmail.smtpserver.core.CommandHandler;
import com.googlecode.asyncmail.smtpserver.hook.HookResult;
import com.googlecode.asyncmail.smtpserver.hook.HookResultHook;
import com.googlecode.asyncmail.smtpserver.hook.HookReturnCode;
import com.googlecode.asyncmail.smtpserver.hook.MailHook;
import com.googlecode.asyncmail.smtpserver.hook.MailParametersHook;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;

/**
 * Handles MAIL command
 */
public class MailCmdHandler extends AbstractHookableCmdHandler implements
        CommandHandler {

    public static final String SENDER = "LOCAL_MAILCMD_SENDER";

    private HashMap<String, MailParametersHook> paramHooks;

    private boolean heloEhloEnforcement;

    private HookResultHook cleanupHook;

    /**
     * Handler method called upon receipt of a MAIL command. Sets up handler to
     * deliver mail as the stated sender.
     * 
     * @param session
     *                SMTP session object
     * @param argument
     *                the argument passed in with the command by the SMTP client
     */
    private SMTPResponse doMAIL(SMTPSession session, String argument) {
        StringBuffer responseBuffer = new StringBuffer();

        MailAddress sender = (MailAddress) session.getState().get(SENDER);
        responseBuffer
                .append(
                        DSNStatus.getStatus(DSNStatus.SUCCESS,
                                DSNStatus.ADDRESS_OTHER)).append(" Sender <");
        if (sender != null) {
            responseBuffer.append(sender);
        }
        responseBuffer.append("> OK");
        doUpdateState(session);
        return new SMTPResponseImpl(SMTPRetCode.MAIL_OK, responseBuffer);
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.core.CommandHandler#getImplCommands()
     */
    public Collection<String> getImplCommands() {
        Collection<String> implCommands = new ArrayList<String>();
        implCommands.add("MAIL");
        return implCommands;
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#doCoreCmd(com.googlecode.asyncmail.smtpserver.IoSession,
     *      java.lang.String, java.lang.String)
     */
    protected SMTPResponse doCoreCmd(SMTPSession session, String command,
            String parameters) {
        return doMAIL(session, parameters);
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#doFilterChecks(com.googlecode.asyncmail.smtpserver.IoSession,
     *      java.lang.String, java.lang.String)
     */
    protected SMTPResponse doFilterChecks(SMTPSession session, String command,
            String parameters) {
        return doMAILFilter(session, parameters);
    }

    /**
     * @param session
     *                SMTP session object
     * @param argument
     *                the argument passed in with the command by the SMTP client
     */
    private SMTPResponse doMAILFilter(SMTPSession session, String argument) {
        String sender = null;
        SMTPHandlerConfigurationData conf = session.getConfigurationData();

        if ((argument != null) && (argument.indexOf(":") > 0)) {
            int colonIndex = argument.indexOf(":");
            sender = argument.substring(colonIndex + 1);
            argument = argument.substring(0, colonIndex);
        }
        if (session.getState().containsKey(SMTPSession.MAIL_ENVELOPE)) {
            return new SMTPResponseImpl(SMTPRetCode.BAD_SEQUENCE, DSNStatus
                    .getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_OTHER)
                    + " Sender already specified");
        } else if (!session.getConnectionState().containsKey(
                SMTPSession.CURRENT_HELO_MODE)
                && getHeloEhloEnforcement()) {
            return new SMTPResponseImpl(SMTPRetCode.BAD_SEQUENCE, DSNStatus
                    .getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_OTHER)
                    + " Need HELO or EHLO before MAIL");
        } else if (argument == null
                || !argument.toUpperCase(Locale.US).equals("FROM")
                || sender == null) {
            return new SMTPResponseImpl(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
                    DSNStatus.getStatus(DSNStatus.PERMANENT,
                            DSNStatus.DELIVERY_INVALID_ARG)
                            + " Usage: MAIL FROM:<sender>");
        } else {
            sender = sender.trim();
            // the next gt after the first lt ... AUTH may add more <>
            int lastChar = sender.indexOf('>', sender.indexOf('<'));
            // Check to see if any options are present and, if so, whether they
            // are correctly formatted
            // (separated from the closing angle bracket by a ' ').
            if ((lastChar > 0) && (sender.length() > lastChar + 2)
                    && (sender.charAt(lastChar + 1) == ' ')) {
                String mailOptionString = sender.substring(lastChar + 2);

                // Remove the options from the sender
                sender = sender.substring(0, lastChar + 1);

                StringTokenizer optionTokenizer = new StringTokenizer(
                        mailOptionString, " ");
                while (optionTokenizer.hasMoreElements()) {
                    String mailOption = optionTokenizer.nextToken();
                    int equalIndex = mailOption.indexOf('=');
                    String mailOptionName = mailOption;
                    String mailOptionValue = "";
                    if (equalIndex > 0) {
                        mailOptionName = mailOption.substring(0, equalIndex)
                                .toUpperCase(Locale.US);
                        mailOptionValue = mailOption.substring(equalIndex + 1);
                    }

                    // Handle the SIZE extension keyword

                    if (paramHooks.containsKey(mailOptionName)) {
                        MailParametersHook hook = paramHooks
                                .get(mailOptionName);
                        SMTPResponse res = calcDefaultSMTPResponse(hook
                                .doMailParameter(session, mailOptionName,
                                        mailOptionValue));
                        if (res != null) {
                            return res;
                        }
                    } else {
                        // Unexpected option attached to the Mail command
                        if (getLogger().isDebugEnabled()) {
                            StringBuffer debugBuffer = new StringBuffer(128)
                                    .append(
                                            "MAIL command had unrecognized/unexpected option ")
                                    .append(mailOptionName).append(
                                            " with value ").append(
                                            mailOptionValue);
                            getLogger().debug(debugBuffer.toString());
                        }
                    }
                }
            }
            if (conf.useAddressBracketsEnforcement()
                    && (!sender.startsWith("<") || !sender.endsWith(">"))) {
                if (getLogger().isErrorEnabled()) {
                    StringBuffer errorBuffer = new StringBuffer(128).append(
                            "Error parsing sender address: ").append(sender)
                            .append(": did not start and end with < >");
                    getLogger().error(errorBuffer.toString());
                }
                return new SMTPResponseImpl(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
                        DSNStatus.getStatus(DSNStatus.PERMANENT,
                                DSNStatus.ADDRESS_SYNTAX_SENDER)
                                + " Syntax error in MAIL command");
            }
            MailAddress senderAddress = null;

            if (conf.useAddressBracketsEnforcement()
                    || (sender.startsWith("<") && sender.endsWith(">"))) {
                // Remove < and >
                sender = sender.substring(1, sender.length() - 1);
            }

            if (sender.length() == 0) {
                // This is the <> case. Let senderAddress == null
            } else {

                if (sender.indexOf("@") < 0) {
                    sender = sender + "@"
                            + session.getConfigurationData().getDefaultDomain();
                }

                try {
                    senderAddress = new MailAddress(sender);
                } catch (Exception pe) {
                    if (getLogger().isErrorEnabled()) {
                        StringBuffer errorBuffer = new StringBuffer(256)
                                .append("Error parsing sender address: ")
                                .append(sender).append(": ").append(
                                        pe.getMessage());
                        getLogger().error(errorBuffer.toString());
                    }
                    return new SMTPResponseImpl(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
                            DSNStatus.getStatus(DSNStatus.PERMANENT,
                                    DSNStatus.ADDRESS_SYNTAX_SENDER)
                                    + " Syntax error in sender address");
                }
            }

            // Store the senderAddress in session map
            session.getState().put(SENDER, senderAddress);
        }
        return null;
    }

    /**
     * Returns whether the remote server needs to send a HELO/EHLO of its
     * senders.
     * 
     * @return whether SMTP authentication is on
     */
    private boolean getHeloEhloEnforcement() {
        return heloEhloEnforcement;
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#getHookInterface()
     */
    protected Class<?> getHookInterface() {
        return MailHook.class;
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#callHook(java.lang.Object,
     *      com.googlecode.asyncmail.smtpserver.IoSession, java.lang.String)
     */
    protected HookResult callHook(Object rawHook, SMTPSession session,
            String parameters) {
        return ((MailHook) rawHook).doMail(session, (MailAddress) session
                .getState().get(SENDER));
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#getMarkerInterfaces()
     */
    public List<Class<?>> getMarkerInterfaces() {
        List<Class<?>> l = super.getMarkerInterfaces();
        l.add(MailParametersHook.class);
        return l;
    }

    /**
     * @see com.googlecode.asyncmail.smtpserver.handler.AbstractHookableCmdHandler#wireExtensions(java.lang.Class,
     *      java.util.List)
     */
    @SuppressWarnings("unchecked")
    public void wireExtensions(Class interfaceName, List extension) {
        if (MailParametersHook.class.equals(interfaceName)) {
            this.paramHooks = new HashMap<String, MailParametersHook>();
            for (Iterator<MailParametersHook> i = extension.iterator(); i
                    .hasNext();) {
                MailParametersHook hook = i.next();
                String[] params = hook.getMailParamNames();
                for (int k = 0; k < params.length; k++) {
                    paramHooks.put(params[k], hook);
                }
            }
        } else {
            super.wireExtensions(interfaceName, extension);
        }
    }

    public void setHeloEhloEnforcement(boolean heloEhloEnforcement) {
        this.heloEhloEnforcement = heloEhloEnforcement;
    }

    @Override
    protected HookResultHook getCleanupHook() {
        if (cleanupHook == null) {
            cleanupHook = new HookResultHook() {

                /**
                 * @see com.googlecode.asyncmail.smtpserver.hook.HookResultHook#onHookResult(com.googlecode.asyncmail.smtpserver.SMTPSession,
                 *      com.googlecode.asyncmail.smtpserver.hook.HookResult,
                 *      java.lang.Object)
                 */
                public HookResult onHookResult(SMTPSession session,
                        HookResult result, Object object) {
                    System.out
                            .println("#########################################: "
                                    + result.getResult());
                    if (result.getResult() == HookReturnCode.DENY
                            || result.getResult() == HookReturnCode.DENYSOFT) {
                        session.getState().remove(SENDER);
                    } else if (result.getResult() == HookReturnCode.DECLINED) {
                        // if the hook declined creates a new MailEnvelope and
                        // sets the
                        // sender
                        doUpdateState(session);
                    }
                    return result;
                }

            };
        }
        return cleanupHook;
    }

    /**
     * @param session
     */
    private void doUpdateState(SMTPSession session) {
        MailEnvelope newEnv = new MailEnvelope();
        newEnv.setSender((MailAddress) session.getState().get(SENDER));
        session.getState().put(SMTPSession.MAIL_ENVELOPE, newEnv);
    }
}
